type Block = number[][]
type BlockType = Block[]

enum keyEvent {
    Nothing = 0,
    L = 1,
    R = 2,
    D = 3,
    U = 4
}

//游戏配置
class Config {
    static width:number = 800   //界面宽度
    static height:number = 600  //界面高度
    static blockSize:number = 25  //方块大小
    static playgroundX:number = 20 //游戏区位置（相对于界面）
    static playgroundY:number = 20 
    static playgroundW:number = 10 //游戏区宽高（游戏坐标 === W * H个格子的区域）
    static playgroundH:number = 20
}

//游戏区的坐标gameX Y 转换为 实际坐标x y
class Position {
    x: number
    y: number 
    gameX: number
    gameY: number
    constructor(gameX:number, gameY:number){
        this.x = gameX * Config.blockSize + Config.playgroundX
        this.y = gameY * Config.blockSize + Config.playgroundY
        this.gameX = gameX
        this.gameY = gameY
    } 
    setY(y:number) {
        this.y = y * Config.blockSize + Config.playgroundY
        this.gameY = y
    }
    setX(x:number){
        this.x = x * Config.blockSize + Config.playgroundX
        this.gameX = x
    }
}

class Game {
    //读取Config
    private width:number = Config.width
    private height:number = Config.height
    private blockSize:number = Config.blockSize
    private playgroundX:number = Config.playgroundX
    private playgroundY:number = Config.playgroundY
    private playgroundW:number = Config.playgroundW
    private playgroundH:number = Config.playgroundH
    
    private keyPressed:keyEvent = keyEvent.Nothing
    //当前块的旋转状态，默认第一个状态
    private blockChange:number = 0

    private canvas:HTMLCanvasElement
    private ctx:CanvasRenderingContext2D|null
    //游戏界面二维数组
    private playGround:number[][]
    //帧率计数器，每次刷新加一，直到达到gameSpeed的数值，游戏进行一帧
    private gameStep:number = 0
    private gameSpeed:number = 120
    private score:number = 0
    //保存2个方块，第0个是正在下落的方块，第1个是下一个方块
    private blocks:BlockType[]
    //当前正在下落的块的位置
    private currentBlockPosition:Position
    //当前正在下落的块
    private currentBlock:Block
    private dead:Boolean = false
    private intervalId:any


    constructor(cav:HTMLCanvasElement){
        this.canvas = cav
        this.ctx = cav.getContext('2d')
        this.blocks = [Blocks.getBlock(), Blocks.getBlock()]
        this.currentBlock = this.blocks[0][this.blockChange]
        this.currentBlockPosition = new Position(4, 0)
        //初始化整个游戏界面的数组
        this.playGround = new Array<Array<number>>(this.playgroundH)
        for (let i = 0; i < this.playGround.length; i++) {
            this.playGround[i] = new Array<number>(this.playgroundW)
            for (let j = 0; j < this.playGround[i].length; j++) {
                this.playGround[i][j] = 0
            }
        }
    }
    run() {
        this.init()
        this.intervalId = setInterval(() => {this.gameCycle()}, 10)
    }
    private init(){
        this.canvas.width = this.width
        this.canvas.height = this.height
        this.canvas.style.cssText = "background: #000; margin: 20px"
        this.canvas.addEventListener('keydown', (e) => {this.onkeyDown(e)}, true)
        this.canvas.focus()
    }
    private onkeyDown(e: KeyboardEvent) {
        switch (e.code) {
            case 'ArrowLeft':
                this.keyPressed = keyEvent.L
                break;
            case 'ArrowRight':
                this.keyPressed = keyEvent.R
                break;
            case 'ArrowDown':
                this.keyPressed = keyEvent.D
                break;
            case 'ArrowUp':
                this.keyPressed = keyEvent.U
            break;
            default:
                break;
        }
    }
    private gameCycle() {
        this.dataUpdate(this.keyPressed)
        this.draw()
    }

    private dataUpdate(key:keyEvent) {
        this.gameStep ++
        if (key == keyEvent.U) {
            //变形一次可以稍微减少一点点下落速度
            this.gameStep -= 2
            //判断是否碰到其他块，如果碰到则不变形
            var flag = true
            var newChange = (this.blockChange + 1) % 4
            //提前获取即将变成的块判断一下是否碰撞
            this.isKnockOtherBlocks(this.blocks[0][newChange],(x,y,px,py) => {
                if (this.currentBlockPosition.gameX + x == px && this.currentBlockPosition.gameY + y == py) {
                    //碰到了已经存在的块
                    flag = false
                }
            })
            if (flag) {
                this.blockChange = newChange
                this.currentBlock = this.blocks[0][this.blockChange]
                //方块变形之后判断是否越界
                this.mapBlock(this.currentBlock, (x, y) => {
                if (this.currentBlockPosition.gameX + x < 0) {
                    //左越界了
                    this.currentBlockPosition.setX(0)
                }
                if(this.currentBlockPosition.gameX + x > this.playgroundW - 1){
                    //右越界了
                    this.currentBlockPosition.setX(this.playgroundW - x - 1)
                }
            })
            }
        }
        if (key == keyEvent.Nothing || key == keyEvent.D) {
            if (this.gameStep == this.gameSpeed || key == keyEvent.D) {
                this.gameStep = 0
                //落到底部的逻辑
                this.isDownToBottom()
            }
        }else{
            switch (key) {
                case keyEvent.L:
                    this.gameStep -= 1
                    this.mapBlock(this.currentBlock, (x, y) => {
                        if(this.currentBlockPosition.gameX + x == 0){
                            //左越界了
                            this.currentBlockPosition.setX(this.currentBlockPosition.gameX + 1)
                        }
                    })
                    this.isKnockOtherBlocks(this.currentBlock,(x,y,px,py) => {
                        //多 -1 表示预判下一步会不会碰到
                        if (this.currentBlockPosition.gameX -1 + x == px && this.currentBlockPosition.gameY + y == py) {
                            //碰到了已经存在的块
                            this.currentBlockPosition.setX(this.currentBlockPosition.gameX + 1)
                        }
                    })
                    this.currentBlockPosition.setX(this.currentBlockPosition.gameX - 1)    
                    break;
                case keyEvent.R:
                    this.gameStep -= 1
                    this.mapBlock(this.currentBlock, (x, y) => {
                        if(this.currentBlockPosition.gameX + x == this.playgroundW - 1){
                            //右越界了
                            this.currentBlockPosition.setX(this.currentBlockPosition.gameX - 1)
                        }
                    })
                    this.isKnockOtherBlocks(this.currentBlock,(x,y,px,py) => {
                        //多 +1 表示预判下一步会不会碰到
                        if (this.currentBlockPosition.gameX + 1 + x == px && this.currentBlockPosition.gameY + y == py) {
                            //碰到了已经存在的块
                            this.currentBlockPosition.setX(this.currentBlockPosition.gameX - 1)
                        }
                    })
                    this.currentBlockPosition.setX(this.currentBlockPosition.gameX + 1)
                    break;
                default:
                    break;
            }
        }
        this.keyPressed = keyEvent.Nothing
    }


    //遍历所有小块，如果存在小块，则callback返回xy坐标
    private mapBlock(block:Block, callback:(x:number,y:number) => void){
        for (let i = 0; i < block.length; i++) {
            const colum = block[i];
            for (let j = 0; j < colum.length; j++) {
                const row = colum[j];
                if (row == 1) {
                    callback(j, i)
                }
            }
        }
    }

    //遍历 当前块和游戏区中的块 xy 当前块坐标，px py游戏区块坐标
    private isKnockOtherBlocks(block:Block, callback:(x:number, y:number, px:number, py: number) => void){
        this.mapBlock(block, (x, y) => {
            this.mapBlock(this.playGround, (px, py) => {
                callback(x, y, px, py)
            })
        })
    }
    
    //控制方块下落，并且判断是否下落到底
    private isDownToBottom() {
        var newY = this.currentBlockPosition.gameY + 1
        this.currentBlockPosition.setY(newY)
        //和其他游戏区的块发生碰撞
        this.isKnockOtherBlocks(this.currentBlock,(x,y,px,py)=>{
            if (this.currentBlockPosition.gameX + x == px && this.currentBlockPosition.gameY + y == py) {
                this.saveBlockPosition()
                this.freshPlayGround()
                this.getNewBlock()
                return
            }
        })

        //落到底部了
        this.mapBlock(this.currentBlock, (x, y) => {
            if (this.currentBlockPosition.gameY + y == this.playgroundH){
                this.saveBlockPosition()
                this.freshPlayGround()
                this.getNewBlock()
                return
            }
        })
    }
    //消块  
    private freshPlayGround() {
        //纪录所有满了的行
        var delIndex:number[] = [];
        //此函数用来判断一行是否全部都是块
        function isAllOne(arr:number[]):Boolean{
            var flag = true
            for (let index = 0; index < arr.length; index++) {
                if (!arr[index]) {
                    flag = false
                }
            }
            return flag
        }
        for (let index = 0; index < this.playGround.length; index++) {
            if (isAllOne(this.playGround[index])) {
                delIndex.push(index)
            }
        }
        //将满了的行删除，并且添加一个新空行
        if (delIndex) {
            delIndex.forEach(i => {
                this.playGround.splice(i, 1)
                var zeroArr = new Array<number>(this.playgroundW)
                for (let index = 0; index < zeroArr.length; index++) {
                    zeroArr[index] = 0
                }
                this.playGround.unshift(zeroArr)
            })
            //这里用一次性删除的行数计分
            switch (delIndex.length) {
                case 1:
                    this.score += 10
                break;
                case 2:
                    this.score += 30
                break;
                case 3:
                    this.score += 60
                break;
                case 4:
                    this.score += 100
                break;
                default:
                    break;
            }
        }
    }
    //下落到底之后将方块固定在游戏界面中
    private saveBlockPosition(){
        var gY = this.currentBlockPosition.gameY
        var gX = this.currentBlockPosition.gameX 
        if (gY != 0) {
            this.mapBlock(this.currentBlock, (x, y) => {
                this.playGround[y + gY - 1][x + gX] = 1
            })
        }
    }
    //获取一个新方块并且刷新当前下落块
    private getNewBlock(){
        this.blocks.shift()
        this.blocks.push(Blocks.getBlock())
        this.blockChange = 0
        this.currentBlock = this.blocks[0][this.blockChange]
        this.currentBlockPosition = new Position(4, 0)
        
        //当新块出生时判断是否与游戏区中的方块相撞，来判断游戏死活
        this.isKnockOtherBlocks(this.currentBlock, (x,y,px,py) => {
            //只要来到这里说明碰撞了，游戏结束
            //this.isDead()
            if (this.currentBlockPosition.gameX + x == px && this.currentBlockPosition.gameY + y == py) {
                this.isDead()
            }
        })
    }
    //游戏结束
    private isDead() {
        clearInterval(this.intervalId)
        this.dead = true
    }
    //画布刷新
    private clear() {
        this.canvas.height = this.canvas.height
    }

    private draw() {
        this.clear()
        if (this.ctx != null) {
            var ct = this.ctx
            //画边框
            ct.rect(this.playgroundX,this.playgroundY,this.playgroundW * this.blockSize,this.playgroundH * this.blockSize);
            ct.strokeStyle = '#fff';
            ct.stroke();
            //画记分板
            ct.font = '30px Verdana'
            ct.fillStyle = '#ffffff'
            ct.fillText('分数：' + this.score.toString(),500,300)
            //画提示区
            ct.rect(500,20,200,200);
            ct.strokeStyle = '#fff';
            ct.stroke();

            ct.fillStyle = "#ff0000"
            //画提示方块
            this.mapBlock(this.blocks[1][0], (x, y) => {
                ct.fillRect(560 + x * this.blockSize, 80 + y * this.blockSize , this.blockSize, this.blockSize)
            })
            //画活动方块
            this.mapBlock(this.currentBlock,(x, y)=>{
                ct.fillRect(this.currentBlockPosition.x + x * this.blockSize, this.currentBlockPosition.y + y * this.blockSize, this.blockSize, this.blockSize)
            })
            // 画 游戏界面上的方块
            this.mapBlock(this.playGround, (x, y) => {
                ct.fillRect(x * this.blockSize + this.playgroundX, y * this.blockSize + this.playgroundY, this.blockSize, this.blockSize)
            })
            //画死亡信息
            if (this.dead) {
                ct.fillStyle = "#ffffff"
                ct.font = '300px'
                ct.fillText('你死了！！,刷新从头开始',200,400)
            }
        }
    }
}

//砖块类，每种砖块对应4个旋转状态
class Blocks {

    static getBlock():BlockType {
        //随机返回一个Block
        // TODO  这里应该可以写点作弊代码，比如让长条出的多一点
        var r = this.getRandomInt(0,6)
        return this.allBlock[r]
    }

    private static getRandomInt(min: number, max: number): number {  
        var Range = max - min;  
        var Rand = Math.random();  
        return(min + Math.round(Rand * Range));  
    }

    private static allBlock:BlockType[] =[
        [
            [
                [1,1,1,1],
                [0,0,0,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [0,0,1,0],
                [0,0,1,0],
                [0,0,1,0],
                [0,0,1,0]
            ],
            [
                [1,1,1,1],
                [0,0,0,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [0,0,1,0],
                [0,0,1,0],
                [0,0,1,0],
                [0,0,1,0]
            ]
        ],[
            [
                [0,1,1,0],
                [0,1,1,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [0,1,1,0],
                [0,1,1,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [0,1,1,0],
                [0,1,1,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [0,1,1,0],
                [0,1,1,0],
                [0,0,0,0],
                [0,0,0,0]
            ]
        ],[
            [
                [0,1,0,0],
                [1,1,1,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [0,1,0,0],
                [0,1,1,0],
                [0,1,0,0],
                [0,0,0,0]
            ],
            [
                [0,0,0,0],
                [1,1,1,0],
                [0,1,0,0],
                [0,0,0,0]
            ],
            [
                [0,1,0,0],
                [1,1,0,0],
                [0,1,0,0],
                [0,0,0,0]
            ]
        ],[
            [
                [1,0,0,0],
                [1,1,1,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [1,1,0,0],
                [1,0,0,0],
                [1,0,0,0],
                [0,0,0,0]
            ],
            [
                [1,1,1,0],
                [0,0,1,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [0,1,0,0],
                [0,1,0,0],
                [1,1,0,0],
                [0,0,0,0]
            ]
        ],[
            [
                [0,0,1,0],
                [1,1,1,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [1,0,0,0],
                [1,0,0,0],
                [1,1,0,0],
                [0,0,0,0]
            ],
            [
                [1,1,1,0],
                [1,0,0,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [1,1,0,0],
                [0,1,0,0],
                [0,1,0,0],
                [0,0,0,0]
            ]
        ],[
            [
                [0,1,1,0],
                [1,1,0,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [1,0,0,0],
                [1,1,0,0],
                [0,1,0,0],
                [0,0,0,0]
            ],
            [
                [0,1,1,0],
                [1,1,0,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [1,0,0,0],
                [1,1,0,0],
                [0,1,0,0],
                [0,0,0,0]
            ]
        ],[
            [
                [1,1,0,0],
                [0,1,1,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [0,1,0,0],
                [1,1,0,0],
                [1,0,0,0],
                [0,0,0,0]
            ],
            [
                [1,1,0,0],
                [0,1,1,0],
                [0,0,0,0],
                [0,0,0,0]
            ],
            [
                [0,1,0,0],
                [1,1,0,0],
                [1,0,0,0],
                [0,0,0,0]
            ]
        ],
    ]
}